#include <assert.h>
#include <inttypes.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
#include <unistd.h>

#ifdef USE_CAPSICUM
#include <sys/capsicum.h>
#endif

#include <err.h>
#include <sysexits.h>

#include "au.h"
#include "adev.h"
#include "common.h"

#ifndef NAMEMAX
#define NAMEMAX 14
#endif
#ifndef BSIZE
#define BSIZE 0x1000
#endif

enum {
	AU2DEV,
	DEV2AU
};

#ifdef SIGINFO
static size_t info_bytes = 0;
#endif

#ifdef SIGINFO
static void	siginfo(int);
#endif

int
main(int argc, char *argv[])
{
#ifdef USE_CAPSICUM
	cap_rights_t rights;
#endif
	int cpipe[2];
	pid_t cpid = 0;
	void *aux = NULL;
	size_t auxs = 0;
	struct adev *a;
	struct au au = AU_INITIALIZER;
	const char *dn = NULL;
	char *p;
	void *b;
	size_t bsize = BSIZE;
	double time = -1;
	ssize_t i;
	ssize_t r;
	ssize_t t;
	uint32_t sps;
	uint32_t enc;
	int dmode;
	int mode;
	int c;

	setprogname(argv[0]);
	dmode = mode = (strstr(getprogname(), "2au") != NULL ||
	    strstr(getprogname(), "rec") != NULL) ? DEV2AU : AU2DEV;
	while ((c = getopt(argc, argv, "RWb:c:d:e:m:r:s:t:")) != -1)
		switch (c) {
		case 'R':
			mode = AU2DEV;
			break;
		case 'W':
			mode = DEV2AU;
			break;
		case 'b':
			if ((bsize = strtosz(optarg)) == 0)
				errx(EX_USAGE, "The buffer size must"
				    " be non-zero");
			break;
		case 'c':
			if (mode != DEV2AU)
				goto usage;
			if ((au.au_chan = strtou32(optarg)) == 0)
				errx(EX_USAGE, "The channel count must"
				    " be non-zero");
			break;
		case 'd':
			dn = optarg;
			break;
		case 'e':
			if (mode != DEV2AU)
				goto usage;
			if ((au.au_enc = au_strenc(optarg))
			    == UINT32_MAX)
				errx(EX_USAGE, "Invalid encoding %s",
				    optarg);
			break;
		case 'r':
			if (mode != DEV2AU)
				goto usage;
			if ((au.au_rate = strtou32(optarg)) == 0)
				errx(EX_USAGE, "The rate must be "
				    "non-zero");
			break;
		case 's':
			if (mode != DEV2AU)
				goto usage;
			if (memacat(&aux, &auxs, optarg,
			    strlen(optarg) + 1) < 0)
				errx(EX_OSERR, "Error reallocating"
				    " auxiliary information");
			break;
		case 't':
			if (mode != DEV2AU)
				goto usage;
			errno = 0;
			if ((time = strtod(optarg, &p)) < 0 ||
			    p == optarg || *p != '\0' || errno != 0)
				err(EX_USAGE, "Invalid number '%s'",
				    optarg);
			break;
		default:
			goto usage;
		}
	if ((b = malloc(bsize)) == NULL)
		err(EX_OSERR, "Could not allocate the buffer");
	if (dn != NULL && strcmp(dn, "-") == 0)
		a = ad_fpopen(mode == AU2DEV ? stdout : stdin,
		    mode == AU2DEV ? A_WRITE : A_READ);
	else {
		if ((c = open("/dev/null",
		    mode == AU2DEV ? O_WRONLY : O_RDONLY)) < 0 ||
		    dup2(c, mode == AU2DEV ? STDOUT_FILENO :
		    STDIN_FILENO) < 0)
			err(EX_OSERR, "Could not replace the standard "
			    "%s as /dev/null", mode == AU2DEV ? "output"
			    : "input");
		(void)close(c);
		a = ad_open(dn, mode == AU2DEV ? A_WRITE : A_READ);
	}
	if (a == NULL)
		err(EX_OSERR, "Could not open the audio device");
#ifdef SIGINFO
	(void)signal(SIGINFO, &siginfo);
#endif
	if (mode == DEV2AU)
		goto dev2au;
au2dev:
	while (nread(STDIN_FILENO, b, AU_SIZE) == AU_SIZE) {
#ifdef SIGINFO
		info_bytes += AU_SIZE;
#endif
		if (au_gethdr(&au, b) != 0)
			errx(EX_DATAERR, "Bad header");
		if (ad_set(a, "rate channels", au.au_rate, au.au_chan)
		    != 2)
			err(EX_SOFTWARE,
			    "Could not set audio parameters");
		if (ad_set(a, "encoding", au.au_enc) != 1) {
			if (cpid > 0)
				abort();
			if (ad_get(a, "encoding", &enc) != 1)
				err(EX_SOFTWARE,
				    "Could not get desired encoding");
			if (pipe(cpipe) == -1)
				err(EX_OSERR, "Could not create pipe");
			switch (cpid = fork()) {
			case -1:
				err(EX_OSERR, "Could not fork process");
			case 0:
				close(cpipe[0]);
				if (dup2(cpipe[1], STDOUT_FILENO) == -1)
					err(EX_OSERR, "Could not "
					    "connect the pipe");
				auconvp(&au, enc, b, bsize);
				err(EX_SOFTWARE,
				    "Could not execute auconv");
			default:
				close(cpipe[1]);
				if (dup2(cpipe[0], STDIN_FILENO) == -1)
					err(EX_OSERR, "Could not "
					    "connect the pipe");
				break;
			}
			continue;
		}
		if (c) {
#ifdef USE_CAPSICUM
			if (cap_enter() == -1 && errno != ENOSYS)
				err(EX_OSERR,
				    "Could not enter capability mode");
			(void)cap_rights_init(&rights, CAP_READ);
			if (cap_rights_limit(STDIN_FILENO, &rights)
			    != 0 && errno != ENOSYS)
				err(EX_OSERR, "Could not limit the "
				    "standard input");
			(void)cap_rights_init(&rights, CAP_WRITE);
			if (cap_rights_limit(STDOUT_FILENO, &rights)
			    != 0 && errno != ENOSYS)
				err(EX_OSERR, "Could not limit the "
				    "standard output");
			if (cap_rights_limit(STDERR_FILENO, &rights)
			    != 0 && errno != ENOSYS)
				err(EX_OSERR, "Could not limit the "
				    "standard error");
#endif /* USE_CAPSICUM */
			ad_limit(a);
			c = 0;
		}
		for (i = AU_SIZE; i < au.au_off; i += r) {
			if ((r = nread(STDIN_FILENO, b,
			    MIN(bsize, au.au_off - i))) <= 0)
				err(EX_DATAERR, "Unexpected EOF");
#ifdef SIGINFO
			info_bytes += r;
#endif
		}
		while (au.au_size > 0 && (r = read(STDIN_FILENO, b,
		    MIN(bsize, au.au_size))) > 0) {
			for (i = 0; i < r; i += t)
				if ((t = ad_write(a,
				    (char *)b + i, r - i)) < 0)
					err(EX_IOERR, "Error while "
					    "playing audio");
				else if (t == 0)
					goto read_ret;
			if (au.au_size != UINT32_MAX)
				au.au_size -= r;
#ifdef SIGINFO
			info_bytes += r;
#endif
		}
		if (r < 0)
			err(EX_DATAERR, "Error while reading audio");
		if (au.au_size == UINT32_MAX)
			break;
	}
read_ret:
	ad_close(a);
	free(b);
	return (EX_OK);
dev2au:
#ifdef USE_CAPSICUM
	if (cap_enter() == -1 && errno != ENOSYS)
		err(EX_OSERR, "Could not enter capibility mode");
	(void)cap_rights_init(&rights, CAP_WRITE);
	if (cap_rights_limit(STDOUT_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard output");
	if (cap_rights_limit(STDERR_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard error");
#endif
	if (ad_limit(a) == -1 && errno != ENOSYS)
		err(EX_OSERR, "Could not limit the audio input");
	if (ad_set(a, "encoding rate channels", au.au_enc, au.au_rate,
	    au.au_chan) != 3)
		err(EX_SOFTWARE, "Could not set the audio parametes");
	au.au_off = AU_SIZE + auxs;
	sps = au_ssize(&au) * au.au_rate;
	do {
		if (time > 0) {
			au.au_size = ~sps + 1;
			if (time >= au.au_size / sps)
				time -= au.au_size / sps;
			else {
				au.au_size = time * (double)sps;
				time = 0;
			}
		}
		(void)au_puthdr(&au, b);
		if (nwrite(STDOUT_FILENO, b, AU_SIZE) != AU_SIZE)
			err(EX_IOERR, "Could not write audio header");
#ifdef SIGINFO
		info_bytes += AU_SIZE;
#endif
		if (auxs > 0 && nwrite(STDOUT_FILENO, aux, auxs) <= 0)
			err(EX_IOERR, "Error while writing to the "
			    "standard output");
		while (au.au_size > 0 && (r = ad_read(a, b,
		    MIN(bsize, au.au_size))) > 0) {
			if (nwrite(STDOUT_FILENO, b, r) != r)
				err(EX_IOERR,
				    "Error while writing audio");
			if (au.au_size != UINT32_MAX)
				au.au_size -= r;
#ifdef SIGINFO
			info_bytes += r;
#endif
		}
		if (r < 0)
			err(EX_IOERR,
			    "Error reading from audio device");
		if (r == 0)
			break;
	} while (au.au_size == UINT32_MAX || time != 0);
	ad_close(a);
	free(b);
	return (EX_OK);
usage:
	if (mode == DEV2AU) {
		(void)fprintf(stderr, "usage: %s %s "
		    "[-b buffer-size] [-d device]\n", getprogname(),
		    dmode == DEV2AU ? "[-W]" : "-W");
		return (EX_USAGE);
	}
	(void)fprintf(stderr, "usage: %s %s [-b buffer-size] "
	    "[-d device] [-e encoding]\n", getprogname(),
	    dmode == AU2DEV ? "[-R]" : "-R");
	for (i = 0; i < strlen(getprogname()); i++)
		(void)fputc(' ', stderr);
	(void)fprintf(stderr, "        [-c channels] [-r rate]"
	    " [-s string] [-t time]\n");
	return (EX_USAGE);
}

#ifdef SIGINFO
void
siginfo(int sig)
{
	/* XXXXXXX: XXXXXXiB processed */
	char b[NAMEMAX + 2 + 8 + 11];
	size_t i;

	i = strlen(getprogname());
	if (i > NAMEMAX) {
		(void)memcpy(b, getprogname(), NAMEMAX - 3);
		(void)memcpy(b + NAMEMAX - 3, "...", 3);
		i = NAMEMAX;
	} else
		(void)memcpy(b, getprogname(), i);
	(void)memcpy(b + i, ": ", 2);
	i += 2;
	i += szwrite(b + i, info_bytes, "B", "iB");
	(void)memcpy(b + i, " processed\n", 11);
	i += 11;
	(void)write(STDERR_FILENO, b, i);
}
#endif
